perf(cli): load the Effect terminal runtime lazily to restore cold-start time - #530
Conversation
…art time agent-bundle --version / --help / argv errors: ~300 ms -> ~60 ms (pre-#505 level); create-agent-bundle --help: ~70 ms -> ~40 ms. The cost was module loading (effect + effect/Terminal + platform-node-shared ~250 ms), not runtime construction (~6 ms), so runCli now builds the terminal runtime on the first command write via a dynamic import of src/effect/cli-runtime.ts, and Commander's own text is written synchronously. The scaffolder splits its Effect/NodeServices/Clack program into scaffold-cli.ts the same way. cli.test.ts records module loads under module.registerHooks and fails if the trivial invocations resolve an effect module.
🦋 Changeset detectedLatest commit: 70bb488 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8bba903ac0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "create-agent-bundle": patch | ||
| --- | ||
|
|
||
| Restore CLI cold-start time by loading the Effect terminal runtime lazily. `agent-bundle --version`, `--help`, and argv errors answer in about 60 ms again (they had regressed to about 300 ms) because the Effect `Terminal` / `Stdio` runtime is now built on a command's first write instead of before argv parsing; command output, `--json` documents, and diagnostics are unchanged. `create-agent-bundle --help` and flag errors no longer evaluate the scaffold bundle (Effect, the Node platform layer, Clack), about 70 ms → 40 ms. |
There was a problem hiding this comment.
Append the required PR number to the changeset
Before merging this publishable-package change, append the actual (#PR) suffix to the summary; it currently ends with a period and therefore violates the repository's required changeset format.
AGENTS.md reference: AGENTS.md:L104-L105
Useful? React with 👍 / 👎.
commit: |
Summary
#505 made
runClibuildmakeScopedEffectRuntime(nodeCliServices)at the composition root, before Commander parsed argv. That loadedeffect,effect/Terminal,effect/Stdio, and the two@effect/platform-node-sharedlayers on every invocation, including--versionand--help: cold start went from ≈60 ms to ≈300 ms. This PR keeps the Terminal/Stdio adoption for command output and loads the runtime lazily, on a command's first write.create-agent-bundlegets the same split (its--helphad gone ≈40 → ≈70 ms across #501/#505).Where the time went
Measured in-process on rc.112 (
nodev22.23.2, unbundledeffectinnode_modules):import 'effect'(barrel)effect/Effect+Layer+Stream+Terminal+Stdio+ManagedRuntime)effect/Terminalalone ≈100 ms)@effect/platform-node-shared/NodeTerminal+/NodeStdioLayer.mergeAll+ManagedRuntime.make+ firstrunPromise(builds the layer)Module loading is the whole cost; runtime/layer construction is noise, so memoizing the runtime buys nothing and the fix is to not load the modules for invocations that never write through them.
What changed
packages/agent-bundle/src/effect/cli-runtime.ts(new):makeCliTerminal(services)— the CLI composition root (makeScopedEffectRuntime(nodeCliServices)behind Promise-shapeddisplay/writeStdout/writeStderr/close). The only module that pulls Effect into the CLI process.packages/agent-bundle/src/cli.ts: importscli-runtime.tstype-only and loads it withimport()on the first command write (cliTerminal()), the same pattern that already keeps./api.tsoff the trivial path. Commander's own text (--help,--version, argv errors) is written synchronously throughconfigureOutputtoCliOutput.argvText(default: the process streams) — the queue-and-replay throughTerminal.displayis gone; Commander only writes before aborting parsing, so nothing interleaves. The runtime closes infinallyonly if a command built it; a foregrounddevsession still keeps it until the session closes.packages/create-agent-bundle/src/index.ts→src/scaffold-cli.ts(new):runCliparses flags and writes--help/ flag errors synchronously toCliStreams, then dynamically importsscaffold-cli.ts(theNodeServices.layerroot, the scaffold program, Clack). Rslib splits it intodist/scaffold-cli.js(447.5 kB); the argv layer is 10.8 kB.tests/support/cli-terminal.tscaptures the argv sinks too;cli-text.test.tsrunsrunCliwith capture streams. New proof inpackages/agent-bundle/tests/cli.test.ts: the built CLI runs under amodule.registerHooksresolve recorder (tests/support/record-module-loads.mjs) and the test fails if--version,--help, or an argv error resolves anyeffect/@effect/platform-node-sharedURL (and checks a real command does).docs/effect-conventions.md: Terminal/Stdio section rewritten around the lazy root, the argv-text exception, the module-load vs construction breakdown, and the re-measured numbers; scaffolder boundary paragraph and the module table row updated.Before / after
Median wall time, built
bin/*.js, Node v22.23.2, same machine, back-to-back runs (node -e 0≈28 ms). The pre-#505 column ise0ae9f0ca^built from source.agent-bundle --versionagent-bundle --helpagent-bundle validate --root examples/host-testagent-bundle doctor --host codex --jsoncreate-agent-bundle --helpModule set resolved by
--versionis now byte-identical to pre-#505 (commander + 9dist/*.jschunks, noeffect)."Commander's help through Terminal vs a sync writer": the existing path is the Terminal path, and any Effect import costs ≥ 240 ms, so there is no lazy variant of it that meets the budget; the sync writer is the pre-#505 path restored.
Review status
70bb48846.chatgpt-codex-connector): reviewed8bba903ac; one P1 thread — append the(#PR)suffix to the changeset — addressed in70bb48846(changeset text only; no code changed). Head70bb48846is not re-reviewed; per the task's instructions no PR comments are posted, so this note stands in for the thread reply and the re-review request.